package com.easyaop.api.support;

import com.easyaop.api.support.loadclass.ClassDefinerAdapter;
import com.easyaop.commons.ASMOpcodes;
import com.easyaop.commons.Constants;
import com.easyaop.commons.asm.ClassEmitter;
import com.easyaop.commons.asm.CodeEmitter;
import com.easyaop.commons.asm.Signature;
import com.easyaop.commons.utils.TypeUtils;

// import org.objectweb.asm.util.TraceClassVisitor;


import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.lang.invoke.CallSite;
import java.lang.invoke.ConstantCallSite;
import java.lang.invoke.LambdaConversionException;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandleInfo;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.util.concurrent.atomic.AtomicInteger;

import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.Type;

import com.easyaop.api.support.loadclass.ClassDefinerAdapter;
import com.easyaop.commons.ASMOpcodes;
import com.easyaop.commons.Constants;
import com.easyaop.commons.asm.ClassEmitter;
import com.easyaop.commons.asm.CodeEmitter;
import com.easyaop.commons.asm.Signature;
import com.easyaop.commons.utils.TypeUtils;

final class CustomInnerClassMetaFactory extends AbstractValidatingLambdaMetaFactory {
    // Used to ensure that each spun class name is unique
    private static final AtomicInteger counter = new AtomicInteger();

    private final String implMethodClassName;        // Name of type containing implementation "CC"

    private final MethodType constructorType;        // Generated class constructor type "(CC)void"

    private final ClassWriter cw;                    // ASM class writer

    private final ClassEmitter ce;

    private final FieldSignature[] fieldsSig;

    public static final String DEBUG_LOCATION_PROPERTY = "easy-aop.debugLocation";

    private static String debugLocation;

    private final Type superType;

    private final Type classType;

    private final Signature interfaceSig;

    private final Signature factoryMethodSig;

    private final String targetMethodName;

    private final MethodType targetMethodType;

    private final ClassDefiner classDefiner;

    public CustomInnerClassMetaFactory(MethodHandles.Lookup caller, MethodType factoryType, String interfaceMethodName,
                                       MethodType interfaceMethodType, MethodHandle implementation,
                                       MethodType dynamicMethodType) throws LambdaConversionException {
        super(caller, factoryType, interfaceMethodName, interfaceMethodType, implementation, dynamicMethodType);

        classType = Type.getType(String.format("L%s;", lambdaClassName(targetClass)));
        superType = Constants.T_OBJECT;

        constructorType = factoryType.changeReturnType(Void.TYPE);

        implMethodClassName = getInternalName(implClass);

        interfaceSig = new Signature(interfaceMethodName, interfaceMethodType);

        factoryMethodSig = new Signature("get$Lambda", factoryType);

        targetMethodName = implInfo.getName();

        MethodType realMethodType = implInfo.getMethodType();
        if (implInfo.getReferenceKind() != MethodHandleInfo.REF_invokeStatic) {
            realMethodType = implInfo.getMethodType().insertParameterTypes(0, implClass);
        }
        targetMethodType = realMethodType;

        cw = new ClassWriter(ClassWriter.COMPUTE_MAXS | ClassWriter.COMPUTE_FRAMES);

        ce = new ClassEmitter(cw);

        int parameterCount = factoryType.parameterCount();
        FieldSignature[] fieldSigs = EMPTY_MEMBER_SIG_ARR;
        if (parameterCount > 0) {
            fieldSigs = new FieldSignature[parameterCount];
            for (int i = 0; i < parameterCount; i++) {
                fieldSigs[i] = new FieldSignature("arg$" + (i + 1), getType(factoryType.parameterType(i)));
            }
        }
        this.fieldsSig = fieldSigs;
        this.classDefiner = ClassDefinerAdapter.newInstance(caller, targetClass);
    }

    private static String lambdaClassName(Class<?> targetClass) {
        String name = targetClass.getName();
        return name.replace('.', '/') + "$$Lambda$" + counter.incrementAndGet();
    }

    @Override
    CallSite buildCallSite() throws LambdaConversionException {
        Class<?> innerClass = spinInnerClass();
        if (classDefiner.majorVersion() < 15) {
            classDefiner.ensureClassInitialized(innerClass);
            return new ConstantCallSite(classDefiner.find(innerClass, factoryMethodSig.getName(),
                    factoryMethodSig.getMethodType()).asType(factoryType));
        }
        return new ConstantCallSite(classDefiner.find(innerClass, Constants.NAME_CTOR, constructorType)
                .asType(factoryType));
    }

    private Class<?> spinInnerClass() throws LambdaConversionException {
        try {
            generateInnerClass();
            final byte[] classBytes = cw.toByteArray();
            if (debugLocation != null) {
                dump(classType.getInternalName(), classBytes);
            }
            return classDefiner.defineClass(classBytes);
        } catch (Throwable ex) {
            throw new LambdaConversionException(ex);
        }
    }

    public static void dump(String internalName, byte[] bytes) {
        new File(debugLocation + File.separator + internalName).getParentFile().mkdirs();
        File file = new File(debugLocation + File.separator + internalName + ".class");

        System.err.println("dump inner class " + file.getPath());

        try (BufferedOutputStream stream = new BufferedOutputStream(new FileOutputStream(file))) {
            stream.write(bytes);
        } catch (IOException ex) {
            throw new RuntimeException(ex);
        }
    }

    private void generateInnerClass() {
        ClassEmitter ce = this.ce;

        ce.begin_class(ASMOpcodes.JDK_VERSION,
                ASMOpcodes.ACC_SUPER + ASMOpcodes.ACC_FINAL + ASMOpcodes.ACC_SYNTHETIC, classType.getInternalName(),
                superType, TypeUtils.parseTypes(interfaceClass), null);

        for (FieldSignature fieldSig : fieldsSig) {
            ce.declare_field(ASMOpcodes.ACC_PRIVATE + ASMOpcodes.ACC_FINAL, fieldSig.getName(), fieldSig.getType(),
                    null);
        }

        generateConstructor();

        generateFactory();

        generateCallMethod();

        generateTargetMethod();

        ce.end_class();
    }

    private void generateConstructor() {
        CodeEmitter ctor = ce.begin_constructor(ASMOpcodes.ACC_PRIVATE,
                constructorType);
        ctor.load_this();
        ctor.super_invoke_constructor();
        int local = 0;
        for (FieldSignature fieldSignature : this.fieldsSig) {
            ctor.load_this();
            ctor.load_arg(local++);
            ctor.putfield(fieldSignature.getName());
        }
        ctor.return_value();
        ctor.end_method();
    }

    private void generateFactory() {
        CodeEmitter cm = ce.begin_method(ASMOpcodes.ACC_PRIVATE | ASMOpcodes.ACC_STATIC,
                factoryMethodSig);
        cm.new_instance_this();
        cm.dup();
        cm.load_args();
        cm.invoke_constructor_this(constructorType);
        cm.return_value();
        cm.end_method();
    }

    private void generateCallMethod() {
        CodeEmitter cm = ce.begin_method(interfaceSig, Type.getType(Exception.class));
        for (FieldSignature fieldSig : fieldsSig) {
            cm.load_this();
            cm.getfield(fieldSig.getName());
        }
        cm.invoke_static_this(targetMethodName, targetMethodType);
        cm.box(targetMethodType.returnType());
        cm.return_value();
        cm.end_method();
    }

    private void generateTargetMethod() {
        ClassLoader classLoader = targetClass.getClassLoader();
        String classFileName = implMethodClassName + ".class";
        try (InputStream inputStream = classLoader.getResourceAsStream(classFileName)) {
            assert inputStream != null;
            ClassReader cr = new ClassReader(inputStream);
            ClassAdapter delegate = new ClassAdapter(ce, implInfo,
                    targetMethodName, targetMethodType, targetClass,
                    classDefiner.majorVersion());
            cr.accept(delegate, ClassReader.EXPAND_FRAMES);
        } catch (IOException ex) {
            throw new RuntimeException(ex);
        }
    }

    static {
        debugLocation = System.getProperty(DEBUG_LOCATION_PROPERTY);
    }
}